package org.mockito;

import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyByte;
import static org.mockito.Matchers.anyChar;
import static org.mockito.Matchers.anyCollection;
import static org.mockito.Matchers.anyDouble;
import static org.mockito.Matchers.anyFloat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyList;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyMap;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anySet;
import static org.mockito.Matchers.anyShort;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hamcrest.Matcher;
import org.mockito.cglib.proxy.Callback;
import org.mockito.cglib.proxy.Factory;
import org.mockito.internal.MockHandler;
import org.mockito.internal.MockitoInvocationHandler;
import org.mockito.internal.creation.MethodInterceptorFilter;
import org.mockito.internal.matchers.Any;
import org.mockito.internal.matchers.Equals;
import org.mockito.internal.matchers.InstanceOf;
import org.mockito.internal.matchers.LocalizedMatcher;
import org.mockito.internal.stubbing.StubbedInvocationMatcher;
import org.mockito.internal.stubbing.answers.Returns;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.OngoingStubbing;

public class DeepMock {

    private static Map<Class<?>, Object> defaultObjects = new HashMap<Class<?>, Object>();
    private static Map<Object, Object> copiedObjects = new HashMap<Object, Object>();

    /**
     * removes all predefined mocks
     */
    public static void clear() {
        defaultObjects = new HashMap<Class<?>, Object>();
        copiedObjects = new HashMap<Object, Object>();
    }

    /**
     * 
     * @param defaultMock
     *            must be a mocked object
     */
    public static void set(Object defaultMock) {
        if (isMock(defaultMock)) {
            defaultObjects.put(defaultMock.getClass().getSuperclass(),
                    defaultMock);
        } else {
            throw new IllegalArgumentException("Not a mocked object: "
                    + defaultMock);
        }
    }

    /**
     * 
     * @param <T>
     * @param classToDefine
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T define(Class<T> classToDefine) {
        final T original = (T) defaultObjects.get(classToDefine);
        if (original != null) {
            T copy = makeCopy(original);

            copiedObjects.clear();
            return copy;
        } else {
            return mock(classToDefine);
        }
    }

    @SuppressWarnings("unchecked")
    private static <T> T makeCopy(final T original) {
        if (isMock(original)) {
            if (!copiedObjects.containsKey(original)) {
                T copy = (T) mock(original.getClass().getSuperclass());
                copiedObjects.put(original, copy);
                copyStubs(original, copy);
            }

            return (T) copiedObjects.get(original);
        } else {
            throw new IllegalArgumentException("Illegal argument: " + original
                    + "is not a mocked object");
        }
    }

    @SuppressWarnings("unchecked")
    private static <T> void copyStubs(final T original, final T copy) {
        for (Callback callback : ((Factory) original).getCallbacks()) {
            if (callback instanceof MethodInterceptorFilter) {
                MockitoInvocationHandler handler = ((MethodInterceptorFilter) callback)
                        .getHandler();
                if (handler instanceof MockHandler) {
                    List<StubbedInvocationMatcher> stubbed = ((MockHandler) handler)
                            .getInvocationContainer().getStubbedInvocations();
                    loopStubs(original, copy, stubbed);
                }
            }
        }
    }

    private static <T> void loopStubs(final T original, final T copy,
            List<StubbedInvocationMatcher> stubbed) {
        for (StubbedInvocationMatcher stubs : stubbed) {
            try {
                final Method method = stubs.getMethod();

                final Object[] args = findArguments(stubs);

                if (method.getReturnType().equals(Void.TYPE)) {
                    voidCall(copy, stubs, method, args);
                } else {
                    returnCall(original, copy, stubs, method, args);
                }
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    @SuppressWarnings("unchecked")
    private static Object[] findArguments(final StubbedInvocationMatcher stubs) {
        final Method method = stubs.getMethod();
        final List<Matcher> matchers = stubs.getMatchers();
        if (matchers.size() > 0) {
            return getMatcherArgs(method, matchers);
        } else {
            return stubs.getInvocation().getArguments();
        }
    }

    @SuppressWarnings("unchecked")
    private static Object parseMatcherObject(
            final Class<? extends Matcher> matcherType,
            final String objectType, final Matcher matcher) {
        if (matcherType.equals(Any.class)) {
            return parseAny(objectType);
        } else if (matcherType.equals(InstanceOf.class)) {
            return parseInstanceOf(matcher);
        } else if (matcherType.equals(Equals.class)) {
            return parseEquals(matcher);
        }

        throw new IllegalStateException("Invalid matcherType: " + matcherType);
    }

    private static Object parseAny(final String objectType) {
        if (objectType.equals(boolean.class.getCanonicalName())) {
            return anyBoolean();
        } else if (objectType.equals(byte.class.getCanonicalName())) {
            return anyByte();
        } else if (objectType.equals(char.class.getCanonicalName())) {
            return anyChar();
        } else if (objectType.equals(short.class.getCanonicalName())) {
            return anyShort();
        } else if (objectType.equals(int.class.getCanonicalName())) {
            return anyInt();
        } else if (objectType.equals(long.class.getCanonicalName())) {
            return anyLong();
        } else if (objectType.equals(float.class.getCanonicalName())) {
            return anyFloat();
        } else if (objectType.equals(double.class.getCanonicalName())) {
            return anyDouble();
        } else if (objectType.equals(List.class.getCanonicalName())) {
            return anyList();
        } else if (objectType.equals(Map.class.getCanonicalName())) {
            return anyMap();
        } else if (objectType.equals(Set.class.getCanonicalName())) {
            return anySet();
        } else if (objectType.equals(Collection.class.getCanonicalName())) {
            return anyCollection();
        } else if (objectType.equals(String.class.getCanonicalName())) {
            return anyString();
        } else {
            return anyObject();
        }
    }

    private static Object parseInstanceOf(final Matcher<?> matcher) {
        String signature = matcher.toString();
        int startIndex = signature.indexOf("(");
        int endIndex = signature.indexOf(")");
        String className = signature.substring(startIndex + 1, endIndex);
        try {
            return isA(Class.forName(className));
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("Invalid className: " + className);
        }
    }

    private static Object parseEquals(final Matcher<?> matcher) {
        return eq(((Equals) ((LocalizedMatcher) matcher).getActualMatcher())
                .getWanted());
    }

    @SuppressWarnings("unchecked")
    private static Object[] getMatcherArgs(final Method method,
            final List<Matcher> matchers) {
        List<String> typeList = parseMethod(method);
        List<Object> matcherList = new ArrayList<Object>(typeList.size());
        int index = 0;
        for (Matcher<?> m : matchers) {
            if (m instanceof LocalizedMatcher) {
                String objectType = typeList.get(index);

                matcherList.add(parseMatcherObject(((LocalizedMatcher) m)
                        .getActualMatcher().getClass(), objectType, m));
            } else if (m instanceof Equals) {
                Equals mm = (Equals) m;
                matcherList.add(mm.getWanted());
            }
            index++;
        }

        return matcherList.toArray();
    }

    private static <T> void voidCall(final T copy,
            final StubbedInvocationMatcher stubs, final Method method,
            Object[] args) throws IllegalAccessException,
            InvocationTargetException {

        when(method.invoke(copy, args)).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(final InvocationOnMock invocation)
                    throws Throwable {
                stubs.answer(new InvocationOnMock() {
                    private static final long serialVersionUID = 1L;

                    @Override
                    public Object getMock() {
                        return copy;
                    }

                    @Override
                    public Method getMethod() {
                        return invocation.getMethod();
                    }

                    @Override
                    public Object[] getArguments() {
                        return invocation.getArguments();
                    }

                    @Override
                    public Object callRealMethod() throws Throwable {
                        return invocation.callRealMethod();
                    }
                });

                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    private static <T> void returnCall(final T original, final T copy,
            final StubbedInvocationMatcher stubs, final Method method,
            Object[] args) throws IllegalAccessException,
            InvocationTargetException, IllegalArgumentException,
            SecurityException, InstantiationException, NoSuchMethodException {

        Collection<Answer> answers = stubs.getAllAnswers();
        List<Answer> copiedAnswers = new LinkedList<Answer>();

        for (Answer answer : answers) {
            if (answer instanceof Returns) {
                Object returnedObject = ((Returns) answer).getObject();

                if (isMock(returnedObject)) {
                    returnedObject = makeCopy(returnedObject);
                    answer = new Returns(returnedObject);
                } else if (method.getReturnType().isArray()) {
                    answer = getArrayAnswer((Object[]) returnedObject);
                } else if (returnedObject instanceof Collection) {
                    answer = getCollectionAnswer(answer,
                            (Collection) returnedObject);
                } else if (returnedObject instanceof Map) {
                    answer = getMapAnswer((Map) returnedObject);
                }
            }

            copiedAnswers.add(answer);
        }

        OngoingStubbing<Object> onGoingAnswers = when(method.invoke(copy, args));
        for (Answer answer : copiedAnswers) {
            onGoingAnswers = onGoingAnswers.thenAnswer(answer);
        }
    }

    private static Answer<?> getArrayAnswer(Object[] object) {
        Object[] copyArray = (Object[]) Array.newInstance(object.getClass()
                .getSuperclass(), object.length);
        int i = 0;
        for (Object original : object) {
            if (isMock(original)) {
                original = makeCopy(original);
            }
            copyArray[i] = original;
            i++;
        }

        return new Returns(copyArray);
    }

    private static Answer<?> getMapAnswer(Map<?, ?> returnedObject) {
        Map<Object, Object> mapOfObjects = new HashMap<Object, Object>();
        for (Object key : ((Map<?, ?>) returnedObject).keySet()) {
            Object value = returnedObject.get(key);
            if (isMock(key)) {
                key = makeCopy(key);
            }

            if (isMock(value)) {
                value = makeCopy(value);
            }

            mapOfObjects.put(key, value);
        }

        return new Returns(mapOfObjects);
    }

    private static Answer<?> getCollectionAnswer(Answer<?> answer,
            Collection<?> returnedObject) throws InstantiationException,
            IllegalAccessException {
        Object object = returnedObject.getClass().newInstance();

        if (object instanceof Set<?>) {
            Set<Object> setOfObjects = new HashSet<Object>();
            for (Object o : returnedObject) {
                if (isMock(o)) {
                    o = makeCopy(o);
                }
                setOfObjects.add(o);
            }

            return new Returns(setOfObjects);
        } else {
            List<Object> listOfObjects = new LinkedList<Object>();
            for (Object o : returnedObject) {
                if (isMock(o)) {
                    o = makeCopy(o);
                }
                listOfObjects.add(o);
            }

            return new Returns(listOfObjects);
        }
    }

    private static <T> boolean isMock(T object) {
        String objectType = object.getClass().getCanonicalName();
        String objectSuperType = object.getClass().getSuperclass()
                .getCanonicalName();

        return objectType.contains(objectSuperType)
                && !objectType.contains("[]");
    }

    private static List<String> parseMethod(Method method) {
        String signature = method.toString();
        int startIndex = signature.indexOf("(");
        int endIndex = signature.indexOf(")");
        String matcherList = signature.substring(startIndex + 1, endIndex);
        String[] matcherArray = matcherList.split(",");
        List<String> typeList = new ArrayList<String>(matcherArray.length);

        for (String arg : matcherArray) {
            typeList.add(arg);
        }

        return typeList;
    }
}

